//  -------------------------------------------------------------------------
//  Copyright (C) 2017 BMW Car IT GmbH
//  -------------------------------------------------------------------------
//  This Source Code Form is subject to the terms of the Mozilla Public
//  License, v. 2.0. If a copy of the MPL was not distributed with this
//  file, You can obtain one at https://mozilla.org/MPL/2.0/.
//  -------------------------------------------------------------------------

#pragma once

#include "RendererMock.h"
#include "internal/RendererLib/RendererEventCollector.h"
#include "internal/SceneGraph/SceneAPI/TextureSampler.h"
#include "internal/SceneGraph/Scene/ActionCollectingScene.h"
#include "internal/SceneGraph/SceneUtils/ResourceUtils.h"
#include "internal/RendererLib/RenderingContext.h"
#include "internal/RendererLib/RendererLogContext.h"
#include "internal/RendererLib/RendererSceneUpdater.h"
#include "internal/RendererLib/SceneStateExecutor.h"
#include "internal/RendererLib/RendererCachedScene.h"
#include "internal/RendererLib/RendererScenes.h"
#include "internal/RendererLib/FrameTimer.h"
#include "internal/RendererLib/SceneExpirationMonitor.h"
#include "ComponentMocks.h"
#include "ResourceDeviceHandleAccessorMock.h"
#include "MockResourceHash.h"
#include "SceneReferenceLogicMock.h"
#include "TestSceneHelper.h"
#include "internal/PlatformAbstraction/PlatformThread.h"
#include "internal/PlatformAbstraction/Collections/Pair.h"
#include "RendererSceneEventSenderMock.h"
#include "RendererSceneUpdaterFacade.h"
#include "internal/Components/SceneUpdate.h"
#include "internal/Watchdog/ThreadAliveNotifierMock.h"

#include <cstdint>
#include <unordered_set>
#include <memory>
#include <string_view>

namespace ramses::internal {

    class ARendererSceneUpdater : public ::testing::Test
    {
    public:
        ARendererSceneUpdater()
            : rendererScenes(rendererEventCollector)
            , expirationMonitor(rendererScenes, rendererEventCollector, rendererStatistics)
            , renderer(Display, rendererScenes, rendererEventCollector, expirationMonitor, rendererStatistics)
            , sceneStateExecutor(renderer, sceneEventSender, rendererEventCollector)
            , rendererSceneUpdater(new RendererSceneUpdaterFacade(Display, renderer.m_platform, renderer, rendererScenes, sceneStateExecutor, rendererEventCollector, frameTimer, expirationMonitor, notifier))
        {
            rendererSceneUpdater->setSceneReferenceLogicHandler(sceneReferenceLogic);
            frameTimer.startFrame();
            rendererSceneUpdater->setLimitFlushesForceApply(ForceApplyFlushesLimit);
            rendererSceneUpdater->setLimitFlushesForceUnsubscribe(ForceUnsubscribeFlushLimit);
            rendererSceneUpdater->setForceMapTimeout(std::chrono::seconds{ 20 }); // some tests fail if scene gets force mapped which can happen when running tests on overloaded system (esp. with valgrind)

            EXPECT_CALL(renderer, setClearColor(_, _)).Times(0);
            // called explicitly from tests, no sense in tracking
            EXPECT_CALL(*rendererSceneUpdater, handleSceneUpdate(_, _)).Times(AnyNumber());
        }

        void TearDown() override
        {
            expectNoEvent();
        }

    protected:
        void update()
        {
            frameTimer.startFrame();
            renderer.getProfilerStatistics().markFrameFinished(std::chrono::microseconds{ 0u });
            EXPECT_CALL(sceneReferenceLogic, update());
            if (rendererSceneUpdater->m_resourceManagerMock)
                EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, hasResourcesToBeUploaded());
            rendererSceneUpdater->updateScenes();
        }

        uint32_t createStagingScene()
        {
            const auto sceneIndex = static_cast<uint32_t>(stagingScene.size());
            const SceneId sceneId(sceneIndex);
            stagingScene.emplace_back(new ActionCollectingScene(SceneInfo{ sceneId }));

            return sceneIndex;
        }

        SceneId getSceneId(uint32_t sceneIndex = 0u)
        {
            return stagingScene[sceneIndex]->getSceneId();
        }

        void publishScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = getSceneId(sceneIndex);
            rendererSceneUpdater->handleScenePublished(sceneId, EScenePublicationMode::LocalAndRemote);
            EXPECT_TRUE(sceneStateExecutor.getSceneState(sceneId) == ESceneState::Published);
            expectInternalSceneStateEvent(ERendererEventType::ScenePublished);
        }

        void requestSceneSubscription(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = getSceneId(sceneIndex);
            EXPECT_CALL(sceneEventSender, sendSubscribeScene(sceneId));
            rendererSceneUpdater->handleSceneSubscriptionRequest(sceneId);
            EXPECT_TRUE(sceneStateExecutor.getSceneState(sceneId) == ESceneState::SubscriptionRequested);
        }

        void receiveScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = getSceneId(sceneIndex);
            rendererSceneUpdater->handleSceneReceived(SceneInfo{ sceneId });
            EXPECT_TRUE(sceneStateExecutor.getSceneState(sceneId) == ESceneState::SubscriptionPending);
            EXPECT_TRUE(rendererScenes.hasScene(sceneId));
        }

        uint32_t createPublishAndSubscribeScene(bool withInitialFlush = true)
        {
            const uint32_t sceneIndex = createStagingScene();
            const SceneId sceneId = getSceneId(sceneIndex);

            publishScene(sceneIndex);
            requestSceneSubscription(sceneIndex);
            receiveScene(sceneIndex);

            if (withInitialFlush)
            {
                //receive initial flush
                performFlush(sceneIndex);
                EXPECT_TRUE(sceneStateExecutor.getSceneState(sceneId) == ESceneState::Subscribed);
                expectInternalSceneStateEvent(ERendererEventType::SceneSubscribed);
            }

            return sceneIndex;
        }

        static void ExpectEventsEqual(const std::initializer_list<ERendererEventType> expectedEvents, const RendererEventVector& actualEvents)
        {
            ASSERT_EQ(expectedEvents.size(), actualEvents.size());
            auto eventIt = actualEvents.cbegin();
            for (auto expectedEvent : expectedEvents)
            {
                EXPECT_EQ(expectedEvent, eventIt->eventType);
                ++eventIt;
            }
        }

        void expectEvents(const std::initializer_list<ERendererEventType> expectedEvents)
        {
            RendererEventVector events;
            RendererEventVector dummy;
            rendererEventCollector.appendAndConsumePendingEvents(events, dummy);
            ExpectEventsEqual(expectedEvents, events);
            EXPECT_TRUE(dummy.empty()) << " expected renderer events only but there are also unchecked scene events";
        }

        void expectSceneEvents(const std::initializer_list<ERendererEventType> expectedEvents)
        {
            RendererEventVector events;
            RendererEventVector dummy;
            rendererEventCollector.appendAndConsumePendingEvents(dummy, events);
            ExpectEventsEqual(expectedEvents, events);
            EXPECT_TRUE(dummy.empty()) << " expected scene events only but there are also unchecked renderer events";
        }

        void expectInternalSceneStateEvents(const std::initializer_list<ERendererEventType> expectedEvents)
        {
            InternalSceneStateEvents events;
            rendererEventCollector.dispatchInternalSceneStateEvents(events);
            ASSERT_EQ(expectedEvents.size(), events.size()) << "internal scene state events";
            auto expectedIt = expectedEvents.begin();
            for (size_t i = 0; i < events.size(); ++i)
                EXPECT_EQ(*expectedIt++, events[i].type) << "internal scene state event #" << i;
        }

        void expectNoEvent()
        {
            RendererEventVector events;
            RendererEventVector sceneEvents;
            rendererEventCollector.appendAndConsumePendingEvents(events, sceneEvents);
            EXPECT_TRUE(events.empty()) << " pending unchecked renderer events";
            EXPECT_TRUE(sceneEvents.empty()) << " pending unchecked scene events";

            expectInternalSceneStateEvents({});
        }

        void expectEvent(ERendererEventType eventType)
        {
            expectEvents({ eventType });
        }

        void expectSceneEvent(ERendererEventType eventType)
        {
            expectSceneEvents({ eventType });
        }

        void expectInternalSceneStateEvent(ERendererEventType eventType)
        {
            expectInternalSceneStateEvents({ eventType });
        }

        void requestMapScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            rendererSceneUpdater->handleSceneMappingRequest(sceneId);
            expectNoEvent();
        }

        void mapScene(uint32_t sceneIndex = 0u)
        {
            requestMapScene(sceneIndex);
            update(); // will set from map requested to being mapped and uploaded (if all pending flushes applied)
            update(); // will set to mapped (if all resources uploaded)
            expectInternalSceneStateEvent(ERendererEventType::SceneMapped);
        }

        bool assignSceneToDisplayBuffer(uint32_t sceneIndex, OffscreenBufferHandle buffer = {}, int32_t renderOrder = 0)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            const bool result = rendererSceneUpdater->handleSceneDisplayBufferAssignmentRequest(sceneId, buffer, renderOrder);
            if (result)
            {
                EXPECT_EQ(renderOrder, renderer.getSceneGlobalOrder(sceneId));
            }

            return result;
        }

        void showScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            rendererSceneUpdater->handleSceneShowRequest(sceneId);
            update();
            expectInternalSceneStateEvent(ERendererEventType::SceneShown);
        }

        void hideScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            rendererSceneUpdater->handleSceneHideRequest(sceneId);
            expectInternalSceneStateEvent(ERendererEventType::SceneHidden);
        }

        void unmapScene(uint32_t sceneIndex = 0u, bool expectResourcesUnload = true)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            if (expectResourcesUnload)
                expectUnloadOfSceneResources(sceneIndex);

            rendererSceneUpdater->handleSceneUnmappingRequest(sceneId);
            expectInternalSceneStateEvent(ERendererEventType::SceneUnmapped);
            expectNoResourceReferencedByScene(sceneIndex);
        }

        void expectNoResourceReferencedByScene(uint32_t sceneIndex = 0u)
        {
            rendererSceneUpdater->m_resourceManagerMock->expectNoResourceReferencesForScene(getSceneId(sceneIndex));
        }

        int getResourceRefCount(ResourceContentHash resource)
        {
            return rendererSceneUpdater->m_resourceManagerMock->getResourceRefCount(resource);
        }

        void expectUnloadOfSceneResources(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadAllSceneResourcesForScene(sceneId));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unreferenceAllResourcesForScene(sceneId));
        }

        void unpublishMapRequestedScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            EXPECT_CALL(sceneEventSender, sendUnsubscribeScene(sceneId));
            rendererSceneUpdater->handleSceneUnpublished(sceneId);
            expectInternalSceneStateEvents({ ERendererEventType::SceneMapFailed, ERendererEventType::SceneUnsubscribedIndirect, ERendererEventType::SceneUnpublished });
            EXPECT_FALSE(renderer.getBufferSceneIsAssignedTo(sceneId).isValid());
        }

        void unpublishMappedScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            expectUnloadOfSceneResources(sceneIndex);
            EXPECT_CALL(sceneEventSender, sendUnsubscribeScene(sceneId));
            rendererSceneUpdater->handleSceneUnpublished(sceneId);
            expectInternalSceneStateEvents({ ERendererEventType::SceneUnmappedIndirect, ERendererEventType::SceneUnsubscribedIndirect, ERendererEventType::SceneUnpublished });
            EXPECT_FALSE(renderer.getBufferSceneIsAssignedTo(sceneId).isValid());
        }

        void unpublishShownScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            EXPECT_CALL(sceneEventSender, sendUnsubscribeScene(sceneId));
            rendererSceneUpdater->handleSceneUnpublished(sceneId);
            expectInternalSceneStateEvents({ ERendererEventType::SceneHiddenIndirect, ERendererEventType::SceneUnmappedIndirect, ERendererEventType::SceneUnsubscribedIndirect, ERendererEventType::SceneUnpublished });

            EXPECT_FALSE(renderer.getBufferSceneIsAssignedTo(sceneId).isValid());
        }

        void unsubscribeMapRequestedScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            rendererSceneUpdater->handleSceneUnsubscriptionRequest(sceneId, true);

            expectInternalSceneStateEvents({ ERendererEventType::SceneMapFailed, ERendererEventType::SceneUnsubscribedIndirect });

            EXPECT_FALSE(renderer.getBufferSceneIsAssignedTo(sceneId).isValid());
        }

        void unsubscribeMappedScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            rendererSceneUpdater->handleSceneUnsubscriptionRequest(sceneId, true);

            expectInternalSceneStateEvents({ ERendererEventType::SceneUnmappedIndirect, ERendererEventType::SceneUnsubscribedIndirect });

            EXPECT_FALSE(renderer.getBufferSceneIsAssignedTo(sceneId).isValid());
        }

        void unsubscribeShownScene(uint32_t sceneIndex = 0u)
        {
            const SceneId sceneId = stagingScene[sceneIndex]->getSceneId();
            rendererSceneUpdater->handleSceneUnsubscriptionRequest(sceneId, true);

            expectInternalSceneStateEvents({ ERendererEventType::SceneHiddenIndirect, ERendererEventType::SceneUnmappedIndirect, ERendererEventType::SceneUnsubscribedIndirect });
            EXPECT_FALSE(renderer.getBufferSceneIsAssignedTo(sceneId).isValid());
        }

        NodeHandle performFlushWithCreateNodeAction(uint32_t sceneIndex = 0u, size_t numNodes = 1u)
        {
            NodeHandle nodeHandle;
            IScene& scene = *stagingScene[sceneIndex];
            SceneAllocateHelper sceneAllocator(scene);
            for (size_t i = 0; i < numNodes; ++i)
                nodeHandle = sceneAllocator.allocateNode(0, {});
            performFlush(sceneIndex);

            return nodeHandle;
        }

        void expectReadPixelsEvents(const std::initializer_list<std::tuple<OffscreenBufferHandle, bool /*success*/>>& expectedEvents)
        {
            RendererEventVector events;
            RendererEventVector dummy;
            rendererEventCollector.appendAndConsumePendingEvents(events, dummy);
            ASSERT_EQ(events.size(), expectedEvents.size());

            auto eventsIt = events.cbegin();
            for (const auto& expectedEvent : expectedEvents)
            {
                const auto& event = *eventsIt;
                EXPECT_EQ(event.offscreenBuffer, std::get<0>(expectedEvent));
                if (std::get<1>(expectedEvent))
                {
                    EXPECT_EQ(event.eventType, ERendererEventType::ReadPixelsFromFramebuffer);
                }
                else
                {
                    EXPECT_EQ(event.eventType, ERendererEventType::ReadPixelsFromFramebufferFailed);
                }
            }

        }

        void performFlush(uint32_t sceneIndex = 0u, SceneVersionTag version = SceneVersionTag::Invalid(), const SceneSizeInformation* sizeInfo = nullptr, const FlushTimeInformation& timeInfo = {}, const SceneReferenceActionVector& sceneRefActions = {})
        {
            ActionCollectingScene& scene = *stagingScene[sceneIndex];
            const SceneSizeInformation newSizeInfo = (sizeInfo ? *sizeInfo : scene.getSceneSizeInformation());
            const SceneSizeInformation currSizeInfo = rendererScenes.hasScene(scene.getSceneId()) ? rendererScenes.getScene(scene.getSceneId()).getSceneSizeInformation() : SceneSizeInformation();

            SceneUpdate update;
            update.actions = (std::move(scene.getSceneActionCollection()));

            SceneActionCollectionCreator creator(update.actions, EFeatureLevel_Latest);

            ResourceContentHashVector resources;
            ResourceChanges resourceChanges;
            ResourceUtils::GetAllResourcesFromScene(resources, scene);
            ResourceUtils::DiffResources(previousResources[sceneIndex], resources, resourceChanges);
            previousResources[sceneIndex].swap(resources);
            resourceChanges.m_sceneResourceActions = scene.getSceneResourceActions();

            for (const auto& resHash : resourceChanges.m_resourcesAdded)
                update.resources.push_back(MockResourceHash::GetManagedResource(resHash));

            update.flushInfos = {1u, version, newSizeInfo, resourceChanges, sceneRefActions, timeInfo, newSizeInfo>currSizeInfo, true};
            scene.resetResourceChanges();
            rendererSceneUpdater->handleSceneUpdate(stagingScene[sceneIndex]->getSceneId(), std::move(update));
        }

        void performFlushWithUniformTimeSync(uint32_t sceneIndex, uint32_t internalTS)
        {
            const FlushTimeInformation timeInfo{ {}, FlushTime::Clock::time_point(std::chrono::milliseconds(internalTS)), FlushTime::Clock::getClockType(), true };
            performFlush(sceneIndex, {}, nullptr, timeInfo);
        }

        void performFlushWithExpiration(uint32_t sceneIndex, uint32_t expirationTS)
        {
            const FlushTimeInformation timeInfo{ FlushTime::Clock::time_point(std::chrono::milliseconds(expirationTS)), {}, FlushTime::Clock::getClockType(), false };
            performFlush(sceneIndex, {}, nullptr, timeInfo);
        }

        bool lastFlushWasAppliedOnRendererScene(uint32_t sceneIndex = 0u)
        {
            return !rendererSceneUpdater->hasPendingFlushes(stagingScene[sceneIndex]->getSceneId());
        }

        void createDisplayAndExpectSuccess(const DisplayConfigData& displayConfig = DisplayConfigData())
        {
            ASSERT_TRUE(rendererSceneUpdater->m_resourceManagerMock == nullptr);
            EXPECT_CALL(*rendererSceneUpdater, createResourceManager(_, _, _, _));
            EXPECT_CALL(renderer.m_platform, createResourceUploadRenderBackend());
            rendererSceneUpdater->createDisplayContext(displayConfig, nullptr);
            EXPECT_TRUE(renderer.hasDisplayController());
            expectEvent(ERendererEventType::DisplayCreated);

            // no offscreen buffers reported uploaded by default
            ON_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferDeviceHandle(_)).WillByDefault(Return(DeviceResourceHandle::Invalid()));
            ON_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferColorBufferDeviceHandle(_)).WillByDefault(Return(DeviceResourceHandle::Invalid()));
            // scene updater queries display/offscreen buffer from device handle when updating modified scenes states - these are invalid for display framebuffer
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferHandle(DisplayControllerMock::FakeFrameBufferHandle)).Times(AnyNumber()).WillRepeatedly(Return(OffscreenBufferHandle::Invalid()));

            // scene updater logic might call resource manager for ref/unref of resources with empty list - no need to track those
            ResourceContentHashVector emptyResources;
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, referenceResourcesForScene(_, emptyResources)).Times(AnyNumber());
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unreferenceResourcesForScene(_, emptyResources)).Times(AnyNumber());

            // querying of resources state happens often, concrete tests can control status reported
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getResourceStatus(_)).Times(AnyNumber());

            // by default skip path triggering upload/unload, concrete tests can override
            ON_CALL(*rendererSceneUpdater->m_resourceManagerMock, hasResourcesToBeUploaded()).WillByDefault(Return(false));
        }

        void destroyDisplay(bool expectFail = false)
        {
            if (!expectFail)
            {
                ASSERT_TRUE(rendererSceneUpdater->m_resourceManagerMock != nullptr);
                EXPECT_TRUE(rendererSceneUpdater->hasResourceManager());
                EXPECT_CALL(renderer.m_platform, destroyResourceUploadRenderBackend());
                EXPECT_CALL(renderer.m_platform, destroyRenderBackend());
                rendererSceneUpdater->m_resourceManagerMock->expectNoResourceReferences();
            }
            rendererSceneUpdater->destroyDisplayContext();
            if (expectFail)
            {
                expectEvent(ERendererEventType::DisplayDestroyFailed);
            }
            else
            {
                EXPECT_FALSE(rendererSceneUpdater->hasResourceManager());
                EXPECT_FALSE(renderer.hasDisplayController());
                expectEvent(ERendererEventType::DisplayDestroyed);
            }
        }

        void createRenderable(uint32_t sceneIndex = 0u, bool withVertexArray = false, bool withTextureSampler = false, EVisibilityMode visibility = EVisibilityMode::Visible, bool withModelUbo = false)
        {
            createRenderableNoFlush(sceneIndex, withVertexArray, withTextureSampler, visibility, withModelUbo);
            performFlush(sceneIndex);
        }

        CameraHandle createCamera(uint32_t sceneIndex = 0u, ECameraProjectionType projectionType = ECameraProjectionType::Perspective)
        {
            IScene& scene = *stagingScene[sceneIndex];
            TestSceneHelper sceneHelper(scene, false, false);
            const auto cameraHandle = sceneHelper.createCamera(projectionType, { 0.1f, 1.f }, { -1.f, 1.f, -1.f, 1.f }, {}, {}, {});

            // these are overridden in tests where more concrete expectations are set
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadUniformBuffer(SemanticUniformBufferHandle{ cameraHandle }, 140u, getSceneId(sceneIndex))).Times(AtMost(1));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, updateUniformBuffer(SemanticUniformBufferHandle{ cameraHandle }, 140u, _, getSceneId(sceneIndex))).Times(AnyNumber());

            return cameraHandle;
        }

        void createRenderableNoFlush(uint32_t sceneIndex = 0u, bool withVertexArray = false, bool withTextureSampler = false, EVisibilityMode visibility = EVisibilityMode::Visible, bool withUbo = false)
        {
            IScene& scene = *stagingScene[sceneIndex];

            scene.allocateRenderGroup(0u, 0u, renderGroupHandle);

            const RenderPassHandle renderPassHandle = scene.allocateRenderPass(0u, {});
            const NodeHandle renderableNode = scene.allocateNode(0u, {});
            scene.allocateRenderable(renderableNode, renderableHandle);

            const CameraHandle cameraHandle = createCamera(sceneIndex, ECameraProjectionType::Perspective);

            scene.addRenderableToRenderGroup(renderGroupHandle, renderableHandle, 0u);
            scene.addRenderGroupToRenderPass(renderPassHandle, renderGroupHandle, 0u);
            scene.setRenderPassCamera(renderPassHandle, cameraHandle);
            scene.setRenderPassEnabled(renderPassHandle, true);

            DataFieldInfoVector uniformDataFields;
            if (withTextureSampler)
            {
                uniformDataFields.push_back(DataFieldInfo{ EDataType::TextureSampler2D });
            }
            if (withUbo)
            {
                uniformDataFields.push_back(DataFieldInfo{ EDataType::Matrix44F, 1u, EFixedSemantics::ModelBlock });
            }
            const DataLayoutHandle uniformDataLayoutHandle = scene.allocateDataLayout(uniformDataFields, MockResourceHash::EffectHash, {});
            scene.allocateDataInstance(uniformDataLayoutHandle, uniformDataInstanceHandle);
            scene.setRenderableDataInstance(renderableHandle, ERenderableDataSlotType_Uniforms, uniformDataInstanceHandle);

            DataFieldInfoVector geometryDataFields;
            geometryDataFields.push_back(DataFieldInfo{ EDataType::Indices, 1u, EFixedSemantics::Indices });
            if (withVertexArray)
            {
                geometryDataFields.push_back(DataFieldInfo{ EDataType::Vector3Buffer, 1u, EFixedSemantics::Invalid });
            }
            const DataLayoutHandle geometryDataLayoutHandle = scene.allocateDataLayout(geometryDataFields, MockResourceHash::EffectHash, {});
            scene.allocateDataInstance(geometryDataLayoutHandle, geometryDataInstanceHandle);
            scene.setRenderableDataInstance(renderableHandle, ERenderableDataSlotType_Geometry, geometryDataInstanceHandle);
            scene.setRenderableVisibility(renderableHandle, visibility);
        }

        void destroyRenderable(uint32_t sceneIndex = 0u)
        {
            IScene& scene = *stagingScene[sceneIndex];
            scene.removeRenderableFromRenderGroup(renderGroupHandle, renderableHandle);
            scene.releaseRenderable(renderableHandle);

            performFlush(sceneIndex);
        }

        void setRenderableResources(uint32_t sceneIndex = 0u, ResourceContentHash indexArrayHash = MockResourceHash::IndexArrayHash)
        {
            setRenderableResourcesNoFlush(sceneIndex, indexArrayHash);
            performFlush(sceneIndex);
        }

        void setRenderableResourcesNoFlush(uint32_t sceneIndex = 0u, ResourceContentHash indexArrayHash = MockResourceHash::IndexArrayHash)
        {
            IScene& scene = *stagingScene[sceneIndex];
            scene.setDataResource(geometryDataInstanceHandle, DataFieldHandle(0u), indexArrayHash, DataBufferHandle::Invalid(), 0u, 0u, 0u);
        }

        DataSlotId createRenderableWithTextureConsumer(uint32_t sceneIndex = 0u)
        {
            expectVertexArrayUploaded(sceneIndex);
            expectResourcesReferencedAndProvided({ MockResourceHash::EffectHash, MockResourceHash::IndexArrayHash }, sceneIndex);
            createRenderable(sceneIndex, false, true);
            setRenderableResources(sceneIndex);
            update();

            IScene& scene = *stagingScene[sceneIndex];
            scene.allocateTextureSampler({ {}, MockResourceHash::TextureHash }, samplerHandle);
            scene.setDataTextureSamplerHandle(uniformDataInstanceHandle, DataFieldHandle(0u), samplerHandle);
            const DataSlotId consumerId(getNextFreeDataSlotIdForDataLinking());
            scene.allocateDataSlot({EDataSlotType::TextureConsumer, consumerId, NodeHandle(), DataInstanceHandle::Invalid(), ResourceContentHash::Invalid(), samplerHandle}, {});

            expectResourcesReferencedAndProvided({ MockResourceHash::TextureHash }, sceneIndex);
            performFlush(sceneIndex);
            update();

            expectSceneEvent(ERendererEventType::SceneDataSlotConsumerCreated);

            return consumerId;
        }

        void removeRenderableResources(uint32_t sceneIndex = 0u)
        {
            IScene& scene = *stagingScene[sceneIndex];
            // it is not allowed to set both resource and data buffer as invalid, so for the purpose of the test cases here we set to non-existent data buffer with handle 0
            scene.setDataResource(geometryDataInstanceHandle, DataFieldHandle(0u), ResourceContentHash::Invalid(), DataBufferHandle(0u), 0u, 0u, 0u);

            performFlush(sceneIndex);
        }

        void expectResourcesReferenced(const ResourceContentHashVector& resources, uint32_t sceneIdx = 0u, int times = 1)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, referenceResourcesForScene(getSceneId(sceneIdx), resources)).Times(times);
        }

        void expectResourcesProvided(int times = 1)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, provideResourceData(_)).Times(times);
        }

        void expectResourcesReferencedAndProvided(const ResourceContentHashVector& resources, uint32_t sceneIdx = 0u)
        {
            // most tests add 1 resource in 1 flush, referencing logic is executed per flush
            for (const auto& res : resources)
                expectResourcesReferenced({ res }, sceneIdx, 1);
            expectResourcesProvided(int(resources.size()));
        }

        void expectResourcesReferencedAndProvided_altogether(const ResourceContentHashVector& resources, uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, referenceResourcesForScene(getSceneId(sceneIdx), resources));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, provideResourceData(_)).Times(int(resources.size()));
        }

        void expectResourcesUnreferenced(const ResourceContentHashVector& resources, uint32_t sceneIdx = 0u, int times = 1)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unreferenceResourcesForScene(getSceneId(sceneIdx), resources)).Times(times);
        }

        void reportResourceAs(ResourceContentHash resource, EResourceStatus status)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getResourceStatus(resource)).Times(AnyNumber()).WillRepeatedly(Return(status));
        }

        void expectOffscreenBufferUploaded(OffscreenBufferHandle buffer, DeviceResourceHandle rtDeviceHandleToReturn = DeviceMock::FakeRenderTargetDeviceHandle, bool doubleBuffered = false, EDepthBufferType depthStencilBufferType = EDepthBufferType::DepthStencil)
        {
            {
                InSequence seq;
                EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferDeviceHandle(buffer)).WillOnce(Return(DeviceResourceHandle::Invalid()));
                EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadOffscreenBuffer(buffer, _, _, _, doubleBuffered, depthStencilBufferType));
                EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferDeviceHandle(buffer)).WillRepeatedly(Return(rtDeviceHandleToReturn));
            }
            // scene updater queries offscreen buffer from device handle when updating modified scenes states
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferHandle(rtDeviceHandleToReturn)).Times(AnyNumber()).WillRepeatedly(Return(buffer));
        }

        void expectDmaOffscreenBufferUploaded(OffscreenBufferHandle buffer, DeviceResourceHandle rtDeviceHandleToReturn, DmaBufferFourccFormat fourccFormat, DmaBufferUsageFlags usageFlags, DmaBufferModifiers modifiers)
        {
            {
                InSequence seq;
                EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferDeviceHandle(buffer)).WillOnce(Return(DeviceResourceHandle::Invalid()));
                EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadDmaOffscreenBuffer(buffer, _, _, fourccFormat, usageFlags, modifiers));
                EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferDeviceHandle(buffer)).WillRepeatedly(Return(rtDeviceHandleToReturn));
            }
            // scene updater queries offscreen buffer from device handle when updating modified scenes states
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferHandle(rtDeviceHandleToReturn)).Times(AnyNumber()).WillRepeatedly(Return(buffer));
        }

        void expectOffscreenBufferDeleted(OffscreenBufferHandle buffer, DeviceResourceHandle deviceHandle = DeviceMock::FakeRenderTargetDeviceHandle)
        {
            InSequence seq;
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferDeviceHandle(buffer)).WillOnce(Return(deviceHandle));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadOffscreenBuffer(buffer));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getOffscreenBufferDeviceHandle(buffer)).WillRepeatedly(Return(DeviceResourceHandle::Invalid()));
        }

        void expectStreamBufferUploaded(StreamBufferHandle buffer, WaylandIviSurfaceId source, DeviceResourceHandle rtDeviceHandleToReturn = DeviceMock::FakeRenderTargetDeviceHandle)
        {
            InSequence seq;
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getStreamBufferDeviceHandle(buffer)).WillOnce(Return(DeviceResourceHandle::Invalid()));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadStreamBuffer(buffer, source));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getStreamBufferDeviceHandle(buffer)).WillRepeatedly(Return(rtDeviceHandleToReturn));
        }

        void expectStreamBufferDeleted(StreamBufferHandle buffer)
        {
            InSequence seq;
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getStreamBufferDeviceHandle(buffer)).WillOnce(Return(DeviceMock::FakeRenderTargetDeviceHandle));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadStreamBuffer(buffer));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getStreamBufferDeviceHandle(buffer)).WillRepeatedly(Return(DeviceResourceHandle::Invalid()));
        }

        void expectExternalBufferUploaded(ExternalBufferHandle buffer, DeviceResourceHandle deviceHandleToReturn = DeviceMock::FakeExternalTextureDeviceHandle)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadExternalBuffer(buffer));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getExternalBufferDeviceHandle(buffer)).Times(AnyNumber()).WillRepeatedly(Return(deviceHandleToReturn));
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, getExternalBufferGlId(buffer)).Times(AnyNumber());
        }

        void expectExternalBufferDeleted(ExternalBufferHandle buffer)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadExternalBuffer(buffer));
        }

        void expectBlitPassUploaded()
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadRenderTargetBuffer(_, getSceneId(0u), _)).Times(2);
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadBlitPassRenderTargets(_, _, _, getSceneId(0u)));
        }

        void expectBlitPassDeleted()
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadRenderTargetBuffer(_, getSceneId(0u))).Times(2);
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadBlitPassRenderTargets(_, getSceneId(0u)));
        }

        void expectStreamTextureUploaded()
        {
            static constexpr DeviceResourceHandle FakeStreamTextureDeviceHandle{ 987 };
            EXPECT_CALL(renderer.m_embeddedCompositingManager, getCompositedTextureDeviceHandleForStreamTexture(_)).WillRepeatedly(Return(FakeStreamTextureDeviceHandle));
        }

        void expectVertexArrayUploaded(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadVertexArray(_, _, getSceneId(sceneIdx)));
        }

        void expectSemanticModelUniformBufferUploaded(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadUniformBuffer(Matcher<SemanticUniformBufferHandle>(_), 64u, getSceneId(sceneIdx)))
                .WillRepeatedly([](SemanticUniformBufferHandle handle, auto /*size*/, auto /*sceneId*/)
                    {
                        EXPECT_EQ(handle.getType(), SemanticUniformBufferHandle::Type::Model);
                        return DeviceMock::FakeUniformBufferDeviceHandle;
                    });
        }
        void expectSemanticCameraUniformBufferUploaded(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadUniformBuffer(Matcher<SemanticUniformBufferHandle>(_), 140u, getSceneId(sceneIdx)))
                .WillRepeatedly([](SemanticUniformBufferHandle handle, auto /*size*/, auto /*sceneId*/)
                    {
                        EXPECT_EQ(handle.getType(), SemanticUniformBufferHandle::Type::Camera);
                        return DeviceMock::FakeUniformBufferDeviceHandle;
                    });
        }
        void expectSemanticModelUniformBufferUpdated(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, updateUniformBuffer(Matcher<SemanticUniformBufferHandle>(_), 64u, _, getSceneId(sceneIdx)))
                .WillOnce([](SemanticUniformBufferHandle handle, auto /*size*/, auto /*data*/, auto /*sceneId*/) { EXPECT_EQ(handle.getType(), SemanticUniformBufferHandle::Type::Model); });
        }
        void expectSemanticCameraUniformBufferUpdated(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, updateUniformBuffer(Matcher<SemanticUniformBufferHandle>(_), 140u, _, getSceneId(sceneIdx)))
                .WillOnce([](SemanticUniformBufferHandle handle, auto /*size*/, auto /*data*/, auto /*sceneId*/) { EXPECT_EQ(handle.getType(), SemanticUniformBufferHandle::Type::Camera); });
        }

        void expectVertexArrayUnloaded(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadVertexArray(_, getSceneId(sceneIdx)));
        }

        void createRenderTargetWithBuffers(uint32_t sceneIndex = 0u, RenderTargetHandle renderTargetHandle = RenderTargetHandle{ 0u }, RenderBufferHandle bufferHandle = RenderBufferHandle{ 0u }, RenderBufferHandle depthHandle = RenderBufferHandle{ 1u })
        {
            IScene& scene = *stagingScene[sceneIndex];
            scene.allocateRenderBuffer({ 16u, 16u, EPixelStorageFormat::RGBA8, ERenderBufferAccessMode::ReadWrite, 0u }, bufferHandle);
            scene.allocateRenderBuffer({ 16u, 16u, EPixelStorageFormat::Depth32, ERenderBufferAccessMode::ReadWrite, 0u }, depthHandle);
            scene.allocateRenderTarget(renderTargetHandle);
            scene.addRenderTargetRenderBuffer(renderTargetHandle, bufferHandle);
            scene.addRenderTargetRenderBuffer(renderTargetHandle, depthHandle);
            performFlush(sceneIndex);
        }

        void expectRenderTargetUploaded(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadRenderTargetBuffer(_, getSceneId(sceneIdx), _)).Times(2);
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, uploadRenderTarget(_, _, getSceneId(sceneIdx)));
        }

        void expectRenderTargetUnloaded(uint32_t sceneIdx = 0u)
        {
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadRenderTargetBuffer(_, getSceneId(sceneIdx))).Times(2);
            EXPECT_CALL(*rendererSceneUpdater->m_resourceManagerMock, unloadRenderTarget(_, getSceneId(sceneIdx)));
        }

        void createBlitPass()
        {
            const BlitPassHandle blitPassHandle(0u);
            IScene& scene = *stagingScene[0];
            const RenderBufferHandle sourceBufferHandle = scene.allocateRenderBuffer({ 16u, 16u, EPixelStorageFormat::RGBA8, ERenderBufferAccessMode::ReadWrite, 0u }, {});
            const RenderBufferHandle destinationBufferHandle = scene.allocateRenderBuffer({ 16u, 16u, EPixelStorageFormat::RGBA8, ERenderBufferAccessMode::ReadWrite, 0u }, {});
            scene.allocateBlitPass(sourceBufferHandle, destinationBufferHandle, blitPassHandle);
            performFlush();
        }

        void destroyRenderTargetWithBuffers(uint32_t sceneIndex = 0u)
        {
            const RenderTargetHandle renderTargetHandle(0u);
            const RenderBufferHandle bufferHandle(0u);
            const RenderBufferHandle depthHandle(1u);
            IScene& scene = *stagingScene[sceneIndex];
            scene.releaseRenderTarget(renderTargetHandle);
            scene.releaseRenderBuffer(bufferHandle);
            scene.releaseRenderBuffer(depthHandle);
            performFlush(sceneIndex);
        }

        void destroyBlitPass()
        {
            const BlitPassHandle blitPassHandle(0u);
            IScene& scene = *stagingScene[0];
            scene.releaseBlitPass(blitPassHandle);
            performFlush();
        }

        std::pair<DataSlotId, DataSlotId> createDataSlots(DataInstanceHandle& dataRefConsumer, float providedValue, DataInstanceHandle* dataRefProviderOut = nullptr, uint32_t providerSceneIdx = 0u, uint32_t consumerSceneIdx = 1u)
        {
            DataInstanceHandle dataRefProvider;

            IScene& scene1 = *stagingScene[providerSceneIdx];
            IScene& scene2 = *stagingScene[consumerSceneIdx];

            const DataLayoutHandle dataLayout1 = scene1.allocateDataLayout({ DataFieldInfo(EDataType::Float) }, ResourceContentHash::Invalid(), {});
            const DataLayoutHandle dataLayout2 = scene2.allocateDataLayout({ DataFieldInfo(EDataType::Float) }, ResourceContentHash::Invalid(), {});
            dataRefProvider = scene1.allocateDataInstance(dataLayout1, {});
            dataRefConsumer = scene2.allocateDataInstance(dataLayout2, {});
            if (nullptr != dataRefProviderOut)
                *dataRefProviderOut = dataRefProvider;

            scene1.setDataSingleFloat(dataRefProvider, DataFieldHandle(0u), providedValue);
            scene2.setDataSingleFloat(dataRefConsumer, DataFieldHandle(0u), 0.f);

            const DataSlotId providerId(getNextFreeDataSlotIdForDataLinking());
            const DataSlotId consumerId(getNextFreeDataSlotIdForDataLinking());
            scene1.allocateDataSlot({ EDataSlotType::DataProvider, providerId, NodeHandle(), dataRefProvider, ResourceContentHash::Invalid(), TextureSamplerHandle() }, {});
            scene2.allocateDataSlot({ EDataSlotType::DataConsumer, consumerId, NodeHandle(), dataRefConsumer, ResourceContentHash::Invalid(), TextureSamplerHandle() }, {});

            performFlush(providerSceneIdx);
            performFlush(consumerSceneIdx);
            update();
            expectSceneEvents({ ERendererEventType::SceneDataSlotConsumerCreated, ERendererEventType::SceneDataSlotProviderCreated });

            return{ providerId, consumerId };
        }

        void createDataSlotsAndLinkThem(DataInstanceHandle& dataRefConsumer, float providedValue, DataInstanceHandle* dataRefProviderOut = nullptr, uint32_t providerSceneIdx = 0u, uint32_t consumerSceneIdx = 1u)
        {
            const auto providerConsumer = createDataSlots(dataRefConsumer, providedValue, dataRefProviderOut, providerSceneIdx, consumerSceneIdx);
            linkProviderToConsumer(getSceneId(providerSceneIdx), providerConsumer.first, getSceneId(consumerSceneIdx), providerConsumer.second);
        }

        void updateProviderDataSlot(uint32_t sceneIdx, DataInstanceHandle dataRefProvider, float newProvidedValue)
        {
            IScene& scene = *stagingScene[sceneIdx];
            scene.setDataSingleFloat(dataRefProvider, DataFieldHandle(0u), newProvidedValue);
        }

        std::pair<DataSlotId, DataSlotId> createTextureSlots(DataSlotHandle* providerDataSlotHandleOut = nullptr, uint32_t providerSceneIdx = 0u, uint32_t consumerSceneIdx = 1u)
        {
            IScene& scene1 = *stagingScene[providerSceneIdx];
            IScene& scene2 = *stagingScene[consumerSceneIdx];

            const TextureSamplerHandle sampler = scene2.allocateTextureSampler({ {}, RenderBufferHandle(999) }, {});

            const DataSlotId providerId(getNextFreeDataSlotIdForDataLinking());
            const DataSlotId consumerId(getNextFreeDataSlotIdForDataLinking());
            DataSlotHandle providerDataSlot = scene1.allocateDataSlot({ EDataSlotType::TextureProvider, providerId, NodeHandle(), DataInstanceHandle::Invalid(), MockResourceHash::TextureHash, TextureSamplerHandle() }, {});
            if (nullptr != providerDataSlotHandleOut)
                *providerDataSlotHandleOut = providerDataSlot;
            scene2.allocateDataSlot({ EDataSlotType::TextureConsumer, consumerId, NodeHandle(), DataInstanceHandle::Invalid(), ResourceContentHash::Invalid(), sampler }, {});

            performFlush(providerSceneIdx);
            performFlush(consumerSceneIdx);
            update();
            expectSceneEvents({ ERendererEventType::SceneDataSlotConsumerCreated, ERendererEventType::SceneDataSlotProviderCreated });

            return{ providerId, consumerId };
        }

        void createTextureSlotsAndLinkThem(DataSlotHandle* providerDataSlotHandleOut = nullptr, uint32_t providerSceneIdx = 0u, uint32_t consumerSceneIdx = 1u, bool expectSuccess = true)
        {
            const auto providerConsumer = createTextureSlots(providerDataSlotHandleOut, providerSceneIdx, consumerSceneIdx);
            linkProviderToConsumer(getSceneId(providerSceneIdx), providerConsumer.first, getSceneId(consumerSceneIdx), providerConsumer.second, expectSuccess);
        }

        DataSlotId createTextureConsumer(uint32_t sceneIndex, TextureSampler::ContentType contentType = TextureSampler::ContentType::RenderBuffer)
        {
            IScene& scene = *stagingScene[sceneIndex];
            const TextureSamplerHandle sampler = scene.allocateTextureSampler({ {}, contentType, {}, MemoryHandle{999u} }, {});
            const DataSlotId consumerId(getNextFreeDataSlotIdForDataLinking());
            scene.allocateDataSlot({ EDataSlotType::TextureConsumer, consumerId, NodeHandle(), DataInstanceHandle::Invalid(), ResourceContentHash::Invalid(), sampler }, {});

            performFlush(sceneIndex);
            update();
            expectSceneEvent(ERendererEventType::SceneDataSlotConsumerCreated);

            return consumerId;
        }

        template <typename BUFFERHANDLE>
        void createBufferLink(BUFFERHANDLE buffer, SceneId consumerSceneId, DataSlotId consumerId, bool expectFail = false)
        {
            rendererSceneUpdater->handleBufferToSceneDataLinkRequest(buffer, consumerSceneId, consumerId);
            expectSceneEvent(expectFail ? ERendererEventType::SceneDataBufferLinkFailed : ERendererEventType::SceneDataBufferLinked);
        }

        void updateProviderTextureSlot(uint32_t sceneIdx, DataSlotHandle providerDataSlot, ResourceContentHash newProvidedValue)
        {
            IScene& scene = *stagingScene[sceneIdx];
            scene.setDataSlotTexture(providerDataSlot, newProvidedValue);
        }

        std::pair<DataSlotId, DataSlotId> createTransformationSlots(TransformHandle* providerTransformHandleOut = nullptr,
                                                                    uint32_t providerSceneIdx = 0u,
                                                                    uint32_t consumerSceneIdx = 1u,
                                                                    TransformHandle* consumerTransformHandleOut = nullptr)
        {
            IScene& scene1 = *stagingScene[providerSceneIdx];
            IScene& scene2 = *stagingScene[consumerSceneIdx];

            const auto nodeHandle1 = scene1.allocateNode(0, {});
            const auto nodeHandle2 = scene2.allocateNode(0, {});

            const auto providerTransformHandle = scene1.allocateTransform(nodeHandle1, {});
            const auto consumerTransformHandle = scene2.allocateTransform(nodeHandle2, {});
            if (nullptr != providerTransformHandleOut)
                *providerTransformHandleOut = providerTransformHandle;
            if (nullptr != consumerTransformHandleOut)
                *consumerTransformHandleOut = consumerTransformHandle;

            const DataSlotId providerId(getNextFreeDataSlotIdForDataLinking());
            const DataSlotId consumerId(getNextFreeDataSlotIdForDataLinking());
            scene1.allocateDataSlot({ EDataSlotType::TransformationProvider, providerId, nodeHandle1, {}, ResourceContentHash::Invalid(), TextureSamplerHandle() }, {});
            scene2.allocateDataSlot({ EDataSlotType::TransformationConsumer, consumerId, nodeHandle2, {}, ResourceContentHash::Invalid(), TextureSamplerHandle() }, {});

            performFlush(providerSceneIdx);
            performFlush(consumerSceneIdx);
            update();
            expectSceneEvents({ ERendererEventType::SceneDataSlotConsumerCreated, ERendererEventType::SceneDataSlotProviderCreated });

            return { providerId, consumerId };
        }

        void createTransformationSlotsAndLinkThem(TransformHandle* providerTransformHandleOut = nullptr, uint32_t providerSceneIdx = 0u, uint32_t consumerSceneIdx = 1u)
        {
            const auto providerConsumer = createTransformationSlots(providerTransformHandleOut, providerSceneIdx, consumerSceneIdx);
            linkProviderToConsumer(getSceneId(providerSceneIdx), providerConsumer.first, getSceneId(consumerSceneIdx), providerConsumer.second);
        }

        void linkProviderToConsumer(SceneId providerSceneId, DataSlotId providerId, SceneId consumerSceneId, DataSlotId consumerId, bool expectSuccess = true)
        {
            rendererSceneUpdater->handleSceneDataLinkRequest(providerSceneId, providerId, consumerSceneId, consumerId);
            expectSceneEvent(expectSuccess ? ERendererEventType::SceneDataLinked : ERendererEventType::SceneDataLinkFailed);
        }

        void unlinkConsumer(SceneId consumerSceneId, DataSlotId consumerId)
        {
            rendererSceneUpdater->handleDataUnlinkRequest(consumerSceneId, consumerId);
            expectSceneEvent(ERendererEventType::SceneDataUnlinked);
        }

        void destroySceneUpdater()
        {
            EXPECT_CALL(renderer.m_platform, destroyRenderBackend());
            rendererSceneUpdater.reset();
            expectEvent(ERendererEventType::DisplayDestroyed);
        }

        void expectRenderableResourcesClean(uint32_t sceneIndex = 0u)
        {
            const RendererCachedScene& scene = rendererScenes.getScene(stagingScene[sceneIndex]->getSceneId());
            EXPECT_FALSE(scene.renderableResourcesDirty(renderableHandle));
        }

        void expectRenderableResourcesDirty(uint32_t sceneIndex = 0u)
        {
            const RendererCachedScene& scene = rendererScenes.getScene(stagingScene[sceneIndex]->getSceneId());
            EXPECT_TRUE(scene.renderableResourcesDirty(renderableHandle));
        }

        void expectModifiedScenesReportedToRenderer(std::initializer_list<uint32_t> indices = {0u})
        {
            for (auto idx : indices)
            {
                const SceneId sceneId = stagingScene[idx]->getSceneId();
                EXPECT_CALL(renderer, markBufferWithSceneForRerender(sceneId));
            }
        }

        void expectNoModifiedScenesReportedToRenderer()
        {
            EXPECT_CALL(renderer, markBufferWithSceneForRerender(_)).Times(0u);
        }

        DataSlotId getNextFreeDataSlotIdForDataLinking()
        {
            return DataSlotId(dataSlotIdForDataLinking.getReference()++);
        }

        OffscreenBufferHandle showAndInitiateInterruptedRendering(uint32_t sceneIdx, uint32_t interruptedSceneIdx)
        {
            // assign scene to double buffered OB
            const OffscreenBufferHandle buffer(111u);
            expectOffscreenBufferUploaded(buffer, DeviceMock::FakeRenderTargetDeviceHandle, true);
            EXPECT_TRUE(rendererSceneUpdater->handleBufferCreateRequest(buffer, 1u, 1u, 0u, true, EDepthBufferType::DepthStencil));
            expectEvent(ERendererEventType::OffscreenBufferCreated);
            EXPECT_TRUE(assignSceneToDisplayBuffer(interruptedSceneIdx, buffer));

            showScene(sceneIdx);
            showScene(interruptedSceneIdx);
            update();

            EXPECT_CALL(*renderer.m_displayController, getRenderBackend()).Times(AnyNumber());
            EXPECT_CALL(*renderer.m_displayController, getDisplayBuffer()).Times(AnyNumber());
            EXPECT_CALL(*renderer.m_displayController, clearBuffer(_, _, _)).Times(AnyNumber());
            EXPECT_CALL(*renderer.m_displayController, getEmbeddedCompositingManager()).Times(AnyNumber());
            EXPECT_CALL(renderer.m_embeddedCompositingManager, notifyClients()).Times(AnyNumber());

            EXPECT_CALL(*renderer.m_displayController, handleWindowEvents());
            EXPECT_CALL(*renderer.m_displayController, canRenderNewFrame()).WillOnce(Return(true));
            EXPECT_CALL(*renderer.m_displayController, swapBuffers());

            EXPECT_CALL(*renderer.m_displayController, renderScene(Ref(rendererScenes.getScene(getSceneId(sceneIdx))), _, nullptr)).WillOnce(
                [](const auto& /*unused*/, const RenderingContext& renderContext, const auto* /*unused*/) {
                EXPECT_EQ(DisplayControllerMock::FakeFrameBufferHandle, renderContext.displayBufferDeviceHandle);
                return SceneRenderExecutionIterator{};
            });

            SceneRenderExecutionIterator interruptedState;
            interruptedState.incrementRenderableIdx();
            EXPECT_CALL(*renderer.m_displayController, renderScene(Ref(rendererScenes.getScene(getSceneId(interruptedSceneIdx))), _, _)).WillOnce(
                [interruptedState](const auto& /*unused*/, const RenderingContext& renderContext, const auto* /*unused*/) {
                EXPECT_EQ(DeviceMock::FakeRenderTargetDeviceHandle, renderContext.displayBufferDeviceHandle);
                return interruptedState;
            });

            renderer.doOneRenderLoop();
            EXPECT_TRUE(renderer.hasAnyBufferWithInterruptedRendering());

            return buffer;
        }

        void doRenderLoop()
        {
            EXPECT_CALL(*renderer.m_displayController, getRenderBackend()).Times(AnyNumber());
            EXPECT_CALL(*renderer.m_displayController, getDisplayBuffer()).Times(AnyNumber());
            EXPECT_CALL(*renderer.m_displayController, clearBuffer(_, _, _)).Times(AnyNumber());
            EXPECT_CALL(*renderer.m_displayController, getEmbeddedCompositingManager()).Times(AnyNumber());
            EXPECT_CALL(renderer.m_embeddedCompositingManager, notifyClients()).Times(AnyNumber());

            EXPECT_CALL(*renderer.m_displayController, handleWindowEvents());
            EXPECT_CALL(*renderer.m_displayController, canRenderNewFrame()).WillOnce(Return(true));
            EXPECT_CALL(*renderer.m_displayController, swapBuffers()).Times(AnyNumber());
            EXPECT_CALL(*renderer.m_displayController, renderScene(_, _, _)).Times(AnyNumber());

            renderer.doOneRenderLoop();
        }

        void readPixels(OffscreenBufferHandle obHandle, uint32_t x, uint32_t y, uint32_t width, uint32_t height, bool fullscreen, bool sendViaDLT, std::string_view filename)
        {
            ScreenshotInfo screenshotInfo;
            screenshotInfo.rectangle.x = x;
            screenshotInfo.rectangle.y = y;
            screenshotInfo.rectangle.width = width;
            screenshotInfo.rectangle.height = height;
            screenshotInfo.fullScreen = fullscreen;
            screenshotInfo.sendViaDLT = sendViaDLT;
            screenshotInfo.filename = filename;

            rendererSceneUpdater->handleReadPixels(obHandle, std::move(screenshotInfo));
        }

        void expectDisplayControllerReadPixels(DeviceResourceHandle deviceHandle, uint32_t x, uint32_t y, uint32_t width, uint32_t height)
        {
            EXPECT_CALL(*renderer.m_displayController, readPixels(deviceHandle, x, y, width, height, _)).WillOnce(Invoke(
                [](auto /*unused*/, auto /*unused*/, auto /*unused*/, auto w, auto h, auto& dataOut) {
                    dataOut.resize(w * h * 4);
                }
            ));
        }

        static constexpr DisplayHandle Display{ 1u };

        const RenderableHandle renderableHandle{ 11 };
        const RenderGroupHandle renderGroupHandle{ 3u };
        const DataInstanceHandle uniformDataInstanceHandle{ 10 };
        const DataInstanceHandle geometryDataInstanceHandle{ 11 };
        UniformBufferHandle cameraUbo;

        const TextureSamplerHandle samplerHandle{ 2 };

        std::vector<std::unique_ptr<ActionCollectingScene>> stagingScene;
        DataSlotId dataSlotIdForDataLinking{9911u};

        static constexpr size_t ForceApplyFlushesLimit = 10u;
        static constexpr size_t ForceUnsubscribeFlushLimit = 20u;

        RendererEventCollector rendererEventCollector;
        RendererScenes rendererScenes;
        SceneExpirationMonitor expirationMonitor;
        StrictMock<SceneReferenceLogicMock> sceneReferenceLogic;
        RendererStatistics rendererStatistics;
        StrictMock<RendererMockWithStrictMockDisplay> renderer;
        StrictMock<RendererSceneEventSenderMock> sceneEventSender;
        FrameTimer frameTimer;
        SceneStateExecutor sceneStateExecutor;
        NiceMock<ThreadAliveNotifierMock> notifier;
        std::unique_ptr<RendererSceneUpdaterFacade> rendererSceneUpdater;

        std::unordered_map<uint32_t, ResourceContentHashVector> previousResources;
    };
}
